#!/usr/bin/python

# dewinfont is copyright 2001,2017 Simon Tatham. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import sys
import string
import struct

# Extract bitmap font data from a Windows .FON or .FNT file.

bytestruct = struct.Struct("B")
wordstruct = struct.Struct("<H")
dwordstruct = struct.Struct("<L")

def frombyte(s):
    return bytestruct.unpack_from(s, 0)[0]
def fromword(s):
    return wordstruct.unpack_from(s, 0)[0]
def fromdword(s):
    return dwordstruct.unpack_from(s, 0)[0]

def asciz(s):
    for length in range(len(s)):
        if frombyte(s[length:length+1]) == 0:
            break
    return s[:length].decode("ASCII",errors = "ignore")

def bool(n):
    if n:
        return "yes"
    else:
        return "no"

class font:
    pass

class char:
    pass

def savefont(f, file):
    "Write out a .fd form of an internal font description."
    file.write("# .fd font description generated by dewinfont.\n\n")
    file.write("facename " + f.facename + "\n")
    file.write("copyright " + f.copyright + "\n\n")
    file.write("height " + "%d"%f.height + "\n")
    file.write("ascent " + "%d"%f.ascent + "\n")
    if f.height == f.pointsize: file.write("# ")
    file.write("pointsize " + "%d"%f.pointsize + "\n\n")
    if not f.italic: file.write("# ")
    file.write("italic " + bool(f.italic) + "\n")
    if not f.underline: file.write("# ")
    file.write("underline " + bool(f.underline) + "\n")
    if not f.strikeout: file.write("# ")
    file.write("strikeout " + bool(f.strikeout) + "\n")
    if f.weight == 400: file.write("# ")
    file.write("weight " + "%d"%f.weight + "\n\n")
    if f.charset == 0: file.write("# ")
    file.write("charset " + "%d"%f.charset + "\n\n")
    for i in range(256):
        file.write("char " + "%d"%i + "\nwidth " + "%d"%f.chars[i].width+"\n")
        if f.chars[i].width != 0:
            for j in range(f.height):
                v = f.chars[i].data[j]
                m = 1 << (f.chars[i].width-1)
                for k in range(f.chars[i].width):
                    if v & m:
                        file.write("O")
                    else:
                        file.write("_")
                    v = v << 1
                file.write("\n")
        file.write("\n")

def writestring(file, ss):
    "Write string data to file"
    file.write(bytes(ss, encoding="ASCII",errors = "ignore"))
    #writestring(file, ss)
    #writestring(file, ss.encode(encoding="ASCII",errors = "ignore"))
    #writestring(file, ss.decode(decoding="ASCII",errors = "ignore"))

def exportfont(f, file, imgname):
    "Write out a .py font data form of an internal font description."
    writestring(file,"# .py font description generated by dewinfont.\n\n")
    writestring(file,"# facename " + f.facename + "\n")
    writestring(file,"# copyright " + f.copyright + "\n\n")
    writestring(file,"# height " + "%d"%f.height + "\n")
    writestring(file,"# ascent " + "%d"%f.ascent + "\n")
    writestring(file,"# ")
    if f.height == f.pointsize: writestring(file,"# ")
    writestring(file,"pointsize " + "%d"%f.pointsize + "\n\n")
    writestring(file,"# ")
    if not f.italic: writestring(file,"# ")
    writestring(file,"italic " + bool(f.italic) + "\n")
    writestring(file,"# ")
    if not f.underline: writestring(file,"# ")
    writestring(file,"underline " + bool(f.underline) + "\n")
    writestring(file,"# ")
    if not f.strikeout: writestring(file,"# ")
    writestring(file,"strikeout " + bool(f.strikeout) + "\n")
    writestring(file,"# ")
    if f.weight == 400: writestring(file,"# ")
    writestring(file,"weight " + "%d"%f.weight + "\n\n")
    writestring(file,"# ")
    if f.charset == 0: writestring(file,"# ")
    writestring(file,"charset " + "%d"%f.charset + "\n\n")

    h_s = 12 // 2
    s_w = 12 * 4
    s_h = 12 * 4
    out_size = 12
    from PIL import Image, ImageDraw, ImageFont

    img = Image.new('RGB', (s_w * 16, s_h * 16) , 0)
    tipfnt = ImageFont.truetype("cour.ttf", 12)
    draw = ImageDraw.Draw(img)

    #write file data;
    for y in range(16):
        for x in range(16):
            draw.line((0, y * s_h, s_w * 16, y * s_h), width=1, fill="#404040")
            draw.line((0, y * s_h + out_size * 2 - 1, s_w * 16, y * s_h + out_size * 2 - 1), width=2, fill="#404040")
            draw.line((0, y * s_h + s_h - 1, s_w * 16, y * s_h + s_h - 1), width=1, fill="#404040")
            #paint grid |||
            draw.line((x * s_w, y * s_h, x * s_w, y * s_h + s_h - 1), width=1, fill="#404040")
            draw.line((x * s_w + out_size * 2 - 1, y * s_h + out_size * 2, x * s_w + out_size * 2 - 1, y * s_h + s_h - 1), width=2, fill="#404040")
            draw.line((x * s_w + s_w - 1, y * s_h, x * s_w + s_w - 1, y * s_h + s_h - 1), width=1, fill="#404040")
            #paint a fixed grid;
            draw.rectangle((x * s_w + out_size * 2 + h_s, y * s_h + out_size * 2 + h_s, x * s_w + out_size * 2 + h_s + out_size - 1, y * s_h + out_size * 2 + h_s + out_size - 1), outline="#101010")

            draw.text((x * s_w + h_s, y * s_h + h_s), "%03d"%(y*16+x), font=tipfnt, fill="#A0A0A0")
            #skip 0-31,128-159
            chrno = y*16+x
            if ((chrno > 31) and (chrno < 128)) or ((chrno > 159) and (chrno < 256)):
                draw.text((x * s_w + h_s, y * s_h + out_size * 2 + h_s), chr(y*16+x), font=tipfnt, fill=(120,120,120))

    for i in range(256):
        #paint grid ---
        writestring(file,"    # ")
        if i >= 0x20:
        	#s = chr(i)
        	#str(chr(i), encoding='utf-8')
        	#s = str(str(chr(i)).encode("ASCII",errors = "ignore"))
        	#s = s.encode("ASCII",errors = "ignore")
        	file.write(struct.pack("B",i))
        writestring(file,"#" + "%d"%i + " width " + "%d"%f.chars[i].width+"\n")
        if f.chars[i].width != 0:
            #write char data;  chr,width,datasize,
            chrsize = (f.chars[i].width + 7) // 8 #height bytes every width 8 pixels;
            writestring(file,"    # Option: fixed,fixed(outheight*2),width,xoffset, Data:")
            for j in range(f.height):
            #for jo in range(chrsize):
            #for jo in range(1,-1,-1):
            #out data;
                v = f.chars[i].data[j]
                v = v << (16 - f.chars[i].width)
                ox=v & 0xFF
                writestring(file," 0x%02x,"%ox)
                ox=(v >> 8) & 0xFF
                writestring(file," 0x%02x,"%ox)
            
            writestring(file,"\n")
            writestring(file,"    0x%02x"%i+", 0x%02x"%(24)+ ", 0x%02x"%f.chars[i].width + ", 0x00,\n")
            #writestring(file,"    0x%02x"%i+", 0x%02x"%(chrsize * f.height)+ ", 0x%02x"%f.chars[i].width + ",")

            #write char img data
            for j in range(f.height):
                #writestring(file,"    # ")
                writestring(file,"    ")
                v = f.chars[i].data[j]
                v = v << (16 - f.chars[i].width)
                #m = 1 << (f.chars[i].width-1)
                m = 1 << 15
                for k in range(16):
                    if (k == 8):
                        writestring(file,", ")
                    if v & m:
                        writestring(file,"X")
                        y = i // 16
                        x = i % 16
                        draw.point((x * s_w + h_s + out_size * 2 + k, y * s_h + out_size * 2 + h_s + j), fill="#FFFFFF")
                        #draw.text((x * s_w + h_s + out_size * 2 + xoffset, y * s_h + out_size * 2 + h_s + yoffset), chr(y*16+x), font=outfnt, fill=(255,255,255))
                    else:
                        writestring(file,"_")
                    v = v << 1
                writestring(file,",\n")
        #writestring(file,"\n")
    img.save(imgname, "BMP")

def dofnt(fnt):
    "Create an internal font description from a .FNT-shaped string."
    f = font()
    f.chars = [None] * 256
    version = fromword(fnt[0:])
    ftype = fromword(fnt[0x42:])
    if ftype & 1:
        sys.stderr.write("This font is a vector font\n")
        return None
    off_facename = fromdword(fnt[0x69:])
    if off_facename < 0 or off_facename > len(fnt):
        sys.stderr.write("Face name not contained within font data")
        return None
    f.facename = asciz(fnt[off_facename:])
    #sys.stdout.write("Face name: {}\n".format(f.facename))
    f.copyright = asciz(fnt[6:66])
    #sys.stdout.write("Copyright: {}\n".format(f.copyright))
    f.pointsize = fromword(fnt[0x44:])
    #sys.stdout.write("Point size: {}\n".format(f.pointsize))
    f.ascent = fromword(fnt[0x4A:])
    #sys.stdout.write("Ascent: {}\n".format(f.ascent))
    f.height = fromword(fnt[0x58:])
    #sys.stdout.write("Height: {}\n".format(f.height))
    f.italic = frombyte(fnt[0x50:]) != 0
    f.underline = frombyte(fnt[0x51:]) != 0
    f.strikeout = frombyte(fnt[0x52:]) != 0
    f.weight = fromword(fnt[0x53:])
    f.charset = frombyte(fnt[0x55:])
    #sys.stdout.write("Attrs: italic={} underline={} strikeout={} weight={}\n".format(f.italic, f.underline, f.strikeout, f.weight))
    #sys.stdout.write("Charset: {}\n".format(f.charset))
    # Read the char table.
    if version == 0x200:
        ctstart = 0x76
        ctsize = 4
    else:
        ctstart = 0x94
        ctsize = 6
    maxwidth = 0
    for i in range(256):
        f.chars[i] = char()
        f.chars[i].width = 0
        f.chars[i].data = [0] * f.height
    firstchar = frombyte(fnt[0x5F:])
    lastchar = frombyte(fnt[0x60:])
    for i in range(firstchar,lastchar+1):
        entry = ctstart + ctsize * (i-firstchar)
        w = fromword(fnt[entry:])
        f.chars[i].width = w
        if ctsize == 4:
            off = fromword(fnt[entry+2:])
        else:
            off = fromdword(fnt[entry+2:])
        #sys.stdout.write("Char {}: width={} offset={} filelen={}".format(i, w, off, len(fnt)))
        widthbytes = (w + 7) // 8
        for j in range(f.height):
            for k in range(widthbytes):
                bytepos = off + k * f.height + j
                #sys.stdout.write("{} -> {:x}\n".format(bytepos, frombyte(fnt[bytepos:])))
                f.chars[i].data[j] = f.chars[i].data[j] << 8
                f.chars[i].data[j] = f.chars[i].data[j] | frombyte(fnt[bytepos:])
            f.chars[i].data[j] = f.chars[i].data[j] >> (8*widthbytes - w)
    return f

def nefon(fon, neoff):
    "Finish splitting up a NE-format FON file."
    ret = []
    # Find the resource table.
    rtable = fromword(fon[neoff + 0x24:]) #80+24>A4;40>1F0E
    rtable = rtable + neoff #1F8E
    # Read the shift count out of the resource table.
    shift = fromword(fon[rtable:])
    # Now loop over the rest of the resource table.
    p = rtable+2
    while 1:
        rtype = fromword(fon[p:])
        if rtype == 0:
            break  # end of resource table
        count = fromword(fon[p+2:])
        p = p + 8  # type, count, 4 bytes reserved
        for i in range(count):
            start = fromword(fon[p:]) << shift
            size = fromword(fon[p+2:]) << shift
            if start < 0 or size < 0 or start+size > len(fon):
                sys.stderr.write("Resource overruns file boundaries\n")
                return None
            if rtype == 0x8008: # this is an actual font
                #sys.stdout.write("Font at {} size {}\n".format(start, size))
                font = dofnt(fon[start:start+size])
                if font == None:
                    sys.stderr.write("Failed to read font resource at %x" \
                    % start)
                    return None
                ret = ret + [font]
            p = p + 12 # start, size, flags, name/id, 4 bytes reserved
    return ret

def pefon(fon, peoff):
    "Finish splitting up a PE-format FON file."
    dirtables=[]
    dataentries=[]
    def gotoffset(off,dirtables=dirtables,dataentries=dataentries):
        if off & 0x80000000:
            off = off &~ 0x80000000
            dirtables.append(off)
        else:
            dataentries.append(off)
    def dodirtable(rsrc, off, rtype, gotoffset=gotoffset):
        number = fromword(rsrc[off+12:]) + fromword(rsrc[off+14:])
        for i in range(number):
            entry = off + 16 + 8*i
            thetype = fromdword(rsrc[entry:])
            theoff = fromdword(rsrc[entry+4:])
            if rtype == -1 or rtype == thetype:
                gotoffset(theoff)

    # We could try finding the Resource Table entry in the Optional
    # Header, but it talks about RVAs instead of file offsets, so
    # it's probably easiest just to go straight to the section table.
    # So let's find the size of the Optional Header, which we can
    # then skip over to find the section table.
    secentries = fromword(fon[peoff+0x06:])
    sectable = peoff + 0x18 + fromword(fon[peoff+0x14:])
    for i in range(secentries):
        secentry = sectable + i * 0x28
        secname = asciz(fon[secentry:secentry+8])
        secrva = fromdword(fon[secentry+0x0C:])
        secsize = fromdword(fon[secentry+0x10:])
        secptr = fromdword(fon[secentry+0x14:])
        if secname == ".rsrc":
            break
    if secname != ".rsrc":
        sys.stderr.write("Unable to locate resource section\n")
        return None
    # Now we've found the resource section, let's throw away the rest.
    rsrc = fon[secptr:secptr+secsize]

    # Now the fun begins. To start with, we must find the initial
    # Resource Directory Table and look up type 0x08 (font) in it.
    # If it yields another Resource Directory Table, we stick the
    # address of that on a list. If it gives a Data Entry, we put
    # that in another list.
    dodirtable(rsrc, 0, 0x08)
    # Now process Resource Directory Tables until no more remain
    # in the list. For each of these tables, we accept _all_ entries
    # in it, and if they point to subtables we stick the subtables in
    # the list, and if they point to Data Entries we put those in
    # the other list.
    while len(dirtables) > 0:
        table = dirtables[0]
        del dirtables[0]
        dodirtable(rsrc, table, -1) # accept all entries
    # Now we should be left with Resource Data Entries. Each of these
    # describes a font.
    ret = []
    for off in dataentries:
        rva = fromdword(rsrc[off:])
        start = rva - secrva
        size = fromdword(rsrc[off+4:])
        font = dofnt(rsrc[start:start+size])
        if font == None:
            sys.stderr.write("Failed to read font resource at %x" % start)
            return None
        ret = ret + [font]
    return ret

def dofon(fon):
    "Split a .FON up into .FNTs and pass each to dofnt."
    # Check the MZ header.
    if str(fon[0:2],encoding='utf-8') != "MZ":
        sys.stderr.write("MZ signature not found\n")
        return None
    # Find the NE header.
    neoff = fromdword(fon[0x3C:])
    if str(fon[neoff:neoff+2],encoding='utf-8') == "NE":
        return nefon(fon, neoff)
    elif str(fon[neoff:neoff+4],encoding='utf-8') == "PE\0\0":
        return pefon(fon, neoff)
    else:
        sys.stderr.write("NE or PE signature not found\n")
        return None

def isfon(data):
    "Determine if a file is a .FON or a .FNT format font."
    if str(data[0:2], encoding='utf-8') == "MZ":
        return 1 # FON
    else:
        return 0 # FNT

a = sys.argv[1:]
options = 1
outfile = None
prefix = None
infile = None
if len(a) == 0:
    sys.stdout.write("usage: dewinfont [-o outfile | -p prefix] file\n")
    sys.exit(0)
while len(a) > 0:
    if a[0] == "--":
        options = 0
        a = a[1:]
    elif options and a[0][0:1] == "-":
        if a[0] == "-o":
            try:
                outfile = a[1]
                a = a[2:]
            except IndexError:
                sys.stderr.write("option -o requires an argument\n")
                sys.exit(1)
        elif a[0] == "-p":
            try:
                prefix = a[1]
                a = a[2:]
            except IndexError:
                sys.stderr.write("option -p requires an argument\n")
                sys.exit(1)
        else:
            sys.stderr.write("ignoring unrecognised option "+a[0]+"\n")
            a = a[1:]
    else:
        if infile != None:
            sys.stderr.write("one input file at once, please\n")
            sys.exit(1)
        infile = a[0]
        a = a[1:]

fp = open(infile, "rb")
data = fp.read()
fp.close()

if isfon(data):
    fonts = dofon(data)
else:
    fonts = [dofnt(data)]

if len(fonts) > 1 and prefix == None:
    sys.stderr.write("more than one font in file; use -p prefix\n")
    print(fonts)
    sys.exit(1)
if outfile == None and prefix == None:
    sys.stderr.write("please specify -o outfile or -p prefix\n")
    sys.exit(1)

for i in range(len(fonts)):
    if len(fonts) == 1 and outfile != None:
        fname = outfile + "_" + "%02d"%fonts[i].height + ".fd"
        img_name = outfile + "_" + "%02d"%fonts[i].height + ".bmp"
    else:
        fname = prefix + "%02d"%i + "_" + "%02d"%fonts[i].height + ".fd"
        img_name = prefix + "%02d"%i + "_" + "%02d"%fonts[i].height + ".bmp"
    if (fonts[i].height == 12) or (fonts[i].height == 13) or (fonts[i].height == 14):
        fp = open(fname, "wb")
        exportfont(fonts[i], fp, img_name)
        fp.close()
    